第19天,今天帶筆電出門一整天,一個字都沒寫,果然不要太高估自己在玩樂的時候會想工作
這裡再舉一個較複雜的例子,本書作者有提供一段檔案上傳的範例程式。當一個檔案上傳時通常畫面上會有兩顆按鈕來控制這個上傳的檔案。按鈕1的功能是暫停以及繼續上傳,按鈕2則是刪除此上傳檔案。就這簡單兩個按鈕配上檔案上傳來說就會有幾個狀態,當檔案在掃描狀態(sign)時是不能進行任何操作的,不能暫停也不能刪除。之後檔案上傳則會是檔案上傳中的狀態(uploading)。上傳之後會進行檢查,檢查完成之後若檔案沒有錯誤則會顯示檔案上傳完成(done),若是檔案有問題就會顯示檔案錯誤(error)。另外,系統會額外提供plugin工具,並且會利用window.external.upload來通知我們程式的狀態。
那們我們故意寫個反例來看看(其實太細節不必在意,只要注意反例與修改之後差異的部分):
先建立剛剛所說的plugin的部分
window.external.upload = function (state) {
console.log(state); //可能為sign、uploading、done、error
};
var plugin = (function () {
var plugin = document.createElement('embed');
plugin.style.display = 'none';
plugin.type = 'application/txftn-webkit';
plugin.sign = function () {
console.log('開始檔案掃描');
};
plugin.pause = function () {
console.log('暫停檔案上傳');
};
plugin.uploading = function () {
console.log('開始檔案上傳');
};
plugin.del = function () {
console.log('刪除檔案上傳');
};
plugin.done = function () {
console.log('檔案上傳完成');
};
document.body.appendChild(plugin);
return plugin;
})();
我們首先來建立Upload物件,狀態我們就用字串來表示
var Upload = function (fileName) {
this.plugin = plugin;
this.fileName = fileName;
this.$button1 = null;
this.$button2 = null;
this.state = 'sign';
};
製作檔案上傳的畫面,包含兩個控制按鈕
Upload.prototype.init = function () {
var self = this;
self.$dom = createUploadView();
self.bindEvent();
function createUploadView() {
var $text = $('<span>').text('檔案名稱:' + self.fileName);
self.$button1 = $('<button>').attr('data-action', 'button1').text('掃描中');
self.$button2 = $('<button>').attr('data-action', 'button2').text('刪除');
return $('<div>').append($text).append(self.$button1).append(self.$button2).appendTo('body');
}
};
針對兩個按鈕綁定點擊事件
Upload.prototype.bindEvent = function () {
var self = this;
self.$button1.click(function () {
if (self.state === 'sign') {
console.log('掃描中,點擊無效');
} else if (self.state === 'uploading') {
self.changeState('pause');
} else if (self.state === 'pause') {
self.changeState('uploading');
} else if (self.state === 'done') {
console.log('檔案已上傳,點擊無效');
} else if (self.state === 'error') {
console.log('檔案上傳失敗,點擊無效');
}
});
self.$button2.click(function () {
if (self.state === 'done' || self.state === 'error' ||
self.state === '') {
self.changeState('del');
} else if (self.state === 'sign') {
console.log('檔案正在掃描中,無法刪除');
} else if (self.state === 'uploading') {
console.log('檔案正在上傳中,無法刪除');
}
});
};
改變狀態的方法,針對每個狀態的行為來實作
Upload.prototype.changeState = function (state) {
switch (state) {
case 'sign':
this.plugin.sign();
this.$button1.text('掃描中,任何操作無效');
break;
case 'uploading':
this.plugin.uploading();
this.$button1.text('正在上傳,點擊暫停');
break;
case 'pause':
this.plugin.pause();
this.$button1.text('已暫停,點擊繼續上傳');
break;
case 'done':
this.plugin.done();
this.$button1.text('上傳完成');
break;
case 'error':
this.$button1.text('上傳失敗');
break;
case 'del':
this.plugin.del();
this.$dom.remove();
console.log('刪除完成');
break;
}
this.state = state;
};
我們實際來模擬上傳一個檔案(123),過稱中我們利用setTimeout來模擬非同步的感覺。
var uploadObj = new Upload('123');
uploadObj.init();
window.external.upload = function (state) {
uploadObj.changeState(state);
};
window.external.upload('sign');
setTimeout(function () {
window.external.upload('uploading');
}, 1000);
setTimeout(function () {
window.external.upload('done');
}, 5000);
那現在我們就用狀態模式來重構一下:
首先建立外掛程式這段沒有改變
window.external.upload = function (state) {
console.log(state); //可能為sign、uploading、done、error
};
var plugin = (function () {
var plugin = document.createElement('embed');
plugin.style.display = 'none';
plugin.type = 'application/txftn-webkit';
plugin.sign = function () {
console.log('開始檔案掃描');
};
plugin.pause = function () {
console.log('暫停檔案上傳');
};
plugin.uploading = function () {
console.log('開始檔案上傳');
};
plugin.del = function () {
console.log('刪除檔案上傳');
};
plugin.done = function () {
console.log('檔案上傳完成');
};
document.body.appendChild(plugin);
return plugin;
})();
需要改的是Upload構造函數,我們等等會為每個狀態建立類別物件,現在在這邊我們要為每一個狀態類別都建立一個實例物件
var Upload = function (fileName) {
this.plugin = plugin;
this.fileName = fileName;
this.$button1 = null;
this.$button2 = null;
this.signState = new SignState(this);
this.uploadingState = new UploadingState(this);
this.pauseState = new PauseState(this);
this.doneState = new DoneState(this);
this.errorState = new ErrorState(this);
this.currentState = this.state;
};
建立畫面物件的部分無變動
Upload.prototype.init = function () {
var self = this;
self.$dom = createUploadView();
self.bindEvent();
function createUploadView() {
var $text = $('<span>').text('檔案名稱:' + self.fileName);
self.$button1 = $('<button>').attr('data-action', 'button1').text('掃描中');
self.$button2 = $('<button>').attr('data-action', 'button2').text('刪除');
return $('<div>').append($text).append(self.$button1).append(self.$button2).appendTo('body');
}
};
這裡要將每次事件都委託給當前的狀態類別來執行
Upload.prototype.bindEvent = function () {
var self = this;
self.$button1.click(function () {
self.currentState.clickHandler1();
});
self.$button2.click(function () {
self.currentState.clickHandler2();
});
};
再來我們移除changeState,把相對應的狀態要做的事情放到Upload類別中
Upload.prototype.sign = function () {
this.plugin.sign();
this.currentState = this.signState;
};
Upload.prototype.uploading = function () {
this.plugin.uploading();
this.$button1.text('正在上傳,點擊暫停');
this.currentState = this.uploadingState;
};
Upload.prototype.pause = function () {
this.plugin.pause();
this.$button1.text('已暫停,點擊繼續上傳');
this.currentState = this.pauseState;
};
Upload.prototype.done = function () {
this.plugin.done();
this.$button1.text('上傳完成');
this.currentState = this.doneState;
};
Upload.prototype.error = function () {
this.$button1.text('上傳失敗');
this.currentState = this.errorState;
};
Upload.prototype.del = function () {
this.plugin.del();
this.$dom.remove();
console.log('刪除完成');
};
接下來做一個工廠,用來避免JS語法中沒有抽象類別帶來的困擾
var StateFactory = (function () {
var state = function () { };
state.prototype.clickHandler1 = function () {
throw new Error('子類別要覆蓋此方法');
};
state.prototype.clickHandler2 = function () {
throw new Error('子類別要覆蓋此方法');
};
return function (parameter) {
var F = function (uploadObj) {
this.uploadObj = uploadObj;
};
F.prototype = new state();
for (var i in parameter) {
F.prototype[i] = parameter[i];
}
return F;
};
})();
接下來製作這些狀態類別
var SignState = StateFactory({
clickHandler1: function () {
console.log('掃描中,點擊無效');
},
clickHandler2: function () {
console.log('檔案正在掃描中,無法刪除');
}
});
var UploadingState = StateFactory({
clickHandler1: function () {
this.uploadObj.pause();
},
clickHandler2: function () {
console.log('檔案正在掃描中,無法刪除');
}
});
var PauseState = StateFactory({
clickHandler1: function () {
this.uploadObj.uploading();
},
clickHandler2: function () {
this.uploadObj.del();
}
});
var DoneState = StateFactory({
clickHandler1: function () {
console.log('檔案已上傳完成,點擊無效');
},
clickHandler2: function () {
this.uploadObj.del();
}
});
var ErrorState = StateFactory({
clickHandler1: function () {
console.log('檔案上傳失敗,點擊無效');
},
clickHandler2: function () {
this.uploadObj.del();
}
});
那我們最後一樣來實際模擬一下,結果應該也是一樣的
var uploadObj = new Upload('123');
uploadObj.init();
window.external.upload = function (state) {
uploadObj[state]();
};
window.external.upload('sign');
setTimeout(function () {
window.external.upload('uploading');
}, 1000);
setTimeout(function () {
window.external.upload('done');
}, 5000);
經過上篇與這篇的範例,我們可以知道狀態模式的優點:由於抽離了狀態,我們可以容易增加狀態和轉換。狀態轉換的邏輯更被放在狀態類別中,在程式中簡化了分支。當然除了優點也會有缺點:如果狀態很多可能幾十種,就會發現系統物件相當多,且邏輯都被分散在個物件之中,不容易在一個地方看出邏輯。物件太多的的話就效能來說是會受到影響的,那可以考慮加入輕量模式共用這些狀態模組。